package com.ar4j.sql;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;

import com.ar4j.ActiveRecord;
import com.ar4j.ActiveRecordContext;
import com.ar4j.ActiveRecordFactory;
import com.ar4j.ActiveRecordMetadata;
import com.ar4j.type.ITypeConverter;
import com.ar4j.util.ClassUtils;

/**
 * A row mapper capable of mapping the contents of a ResultSet row to an active record instance.
 */
public class ActiveRecordRowMapper<T extends ActiveRecord<T>> implements RowMapper<T>  {
  private ActiveRecordMetadata metadata;
  private ActiveRecordContext prototypeContext;

  /**
   * Create a new mapper using the active record's metadata and a given prototype context.
   */
  public ActiveRecordRowMapper(ActiveRecordMetadata metadata, ActiveRecordContext prototypeContext) {
    this.metadata = metadata;
    this.prototypeContext = prototypeContext;
  }

  /**
   * Create a new active record instance, settings its properties to values retrieved from the
   * given result set. Conversions and type coercions are performed as needed.
   */
  @SuppressWarnings("unchecked")
  @Override
  public T mapRow(ResultSet rs, int rowNum) throws SQLException {
    
    T out = ActiveRecordFactory.getFactory().getActiveRecord((Class<T>) metadata.getBaseClass(), prototypeContext);
    
    ResultSetMetaData rsmd = rs.getMetaData();
    int columnCount = rsmd.getColumnCount();
    for(int i = 1; i <= columnCount; i++) {
      String column = JdbcUtils.lookupColumnName(rsmd, i).toLowerCase();
      String property = metadata.getColumnToPropertyMap().get(column);
      if(property == null) {
        throw new SQLException("Unknown column in result set: '" + column + " for active record: " + metadata.getBaseClass());
      }
      
      Class<?> propertyType = metadata.getPropertyTypeMap().get(property);
      Class<?> storageType = propertyType;
      
      // see if we have a converter and use its storage type if needed
      ITypeConverter<Object, Object> converter = (ITypeConverter<Object, Object>) metadata.getPropertyTypeConvertorMap().get(property);
      if(converter != null) {
        storageType = converter.getStorageType();
      }
      
      // extract value from the result set and use the specified converter if needed
      Object value = SqlUtils.extractValueFromResultSet(rs, i, storageType);
      if(converter != null) {
        value = converter.convertFromStorage(value, (Class<Object>) propertyType);
      } 
      
      // do a final coercion to the property type and set its value
      value = ClassUtils.coerceValueToType(value, propertyType);
      out.setProperty(property, value);      
    }
    
    return out;
  }
  
  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }
}
